package com.fuhailiu.opengl.widget;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.Arrays;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Color;
import android.opengl.GLES30;

import com.fuhailiu.opengl.utils.BenchmarkUtil;
import com.fuhailiu.opengl.utils.GlUtil;
import com.fuhailiu.opengl.utils.LogUtil;
import com.fuhailiu.opengl.utils.MatrixUtil;
import com.fuhailiu.opengl.utils.ShaderUtil;
import com.fuhailiu.opengl.view.GLView;

/**
 * testing Pixel Buffer Object for unpacking (uploading) pixel data to PBO. It uses 2 PBOs to optimize uploading
 * pipeline; application to PBO, and PBO to texture object.
 */
public class GLPBOUnPack extends GLView {

	private final String TAG = getClass().getSimpleName();

	public GLPBOUnPack(Context context) {
		super(context);
		initVertexData();
		initShader();
		initTextureData();
		initTexture();
		initPBO();
	}

	public final float x = 0.4f;
	public final float y = 0.4f;

	// 顶点坐标数据
	private float vertexCoords[] = new float[] { 
			-x, -y, 0f, // 0 bottom left
			x, -y, 0f, // 1 bottom right
			-x, y, 0f, // 2 top left
			x, y, 0f, // 3 top right
	};

	// 纹理坐标数据
	private float textureCoords[] = new float[] { 
			0.0f, 1.0f, // 0 bottom left
			1.0f, 1.0f, // 1 bottom right
			0.0f, 0.0f, // 2 top left
			1.0f, 0.0f, // 3 top right
	};

	// 顶点坐标数据缓冲
	private FloatBuffer mBufferVertex;
	// 纹理坐标数据缓冲
	private FloatBuffer mBufferTexture;

	private void initVertexData() {
		mBufferVertex = GlUtil.createFloatBuffer(vertexCoords);
		mBufferTexture = GlUtil.createFloatBuffer(textureCoords);
	}

	private final String VERTEX_SHADER = 
			"uniform mat4 uMVPMatrix;\n" + 
			"attribute vec3 aPosition;\n" + 
			"attribute vec2 aTextureCoord;\n" + 
			"varying vec2 vTextureCoord;\n" + 
			"void main() {\n" + 
			"    gl_Position = uMVPMatrix * vec4(aPosition, 1);\n" + 
			"    vTextureCoord = aTextureCoord;\n" + 
			"}\n";

	private final String FRAGMENT_SHADER = 
				"precision mediump float;\n" + 
				"varying vec2 vTextureCoord;\n" + 
				"uniform sampler2D sTexture;\n" + 
				"void main() {\n" + 
				"    gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + 
				"}\n";

	// 自定义渲染管线程序id
	private int handleProgram;
	// 顶点位置属性引用id
	private int handlePosition;
	// 纹理坐标属性引用id
	private int handleTextureCoord;
	// 总变换矩阵引用id
	private int handleMVPMatrix;

	private void initShader() {
		LogUtil.LogD(TAG, "initShader   <---");

		// 基于顶点着色器与片元着色器创建程序
		handleProgram = ShaderUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
		// 获取程序中顶点位置属性引用id
		handlePosition = GLES30.glGetAttribLocation(handleProgram, "aPosition");
		// 获取程序中纹理坐标引用id
		handleTextureCoord = GLES30.glGetAttribLocation(handleProgram, "aTextureCoord");
		// 获取程序中总变换矩阵引用id
		handleMVPMatrix = GLES30.glGetUniformLocation(handleProgram, "uMVPMatrix");

		LogUtil.LogD(TAG, "initShader   --->");
	}

	private final int DATA_SIZE = IMAGE_WIDTH * IMAGE_HEIGHT * GlUtil.BYTES_PER_PIXEL;
	private ByteBuffer mBufferPixelData;

	private void initTextureData() {
		LogUtil.LogD(TAG, "initTextureData   <---");

		byte[] data = new byte[DATA_SIZE];
		// white
		byte value = (byte) 255;
		Arrays.fill(data, value);
		mBufferPixelData = GlUtil.createByteBuffer(data);

		LogUtil.LogD(TAG, "initTextureData   --->");
	}

	private int mTextureId;

	private static final int IMAGE_WIDTH = 1024;
	private static final int IMAGE_HEIGHT = 1024;

	private void initTexture() {
		LogUtil.LogD(TAG, "initTexture   <---");

		int[] textureIds = new int[1];
		GLES30.glGenTextures(1, textureIds, 0);

		int textureId = textureIds[0];
		GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId);
		GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
		GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
		GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
		GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
		GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, IMAGE_WIDTH, IMAGE_HEIGHT, 0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, mBufferPixelData);

		mTextureId = textureId;

		LogUtil.LogD(TAG, "initTexture   --->");
	}

	private final int bufferCount = 2;
	private int[] pboIds;

	private void initPBO() {
		LogUtil.LogD(TAG, "initPBO   <---");

		// create 2 pixel buffer objects, glBufferData with NULL reserves only memory space.
		pboIds = new int[bufferCount];
		GLES30.glGenBuffers(bufferCount, pboIds, 0);
		GlUtil.checkGlError("glGenBuffers");

		for (int i = 0; i < bufferCount; i++) {
			GLES30.glBindBuffer(GLES30.GL_PIXEL_UNPACK_BUFFER, pboIds[i]);
			GLES30.glBufferData(GLES30.GL_PIXEL_UNPACK_BUFFER, DATA_SIZE, mBufferPixelData, GLES30.GL_STREAM_DRAW);
		}

		// 解除绑定
		GLES30.glBindBuffer(GLES30.GL_PIXEL_UNPACK_BUFFER, 0);

		LogUtil.LogD(TAG, "initPBO   --->");
	}

	@Override
	public void release() {
		LogUtil.LogD(TAG, "release   <---");

		super.release();

		GLES30.glDeleteTextures(1, new int[] { mTextureId }, 0);
		GlUtil.checkGlError("glDeleteTextures");

		GLES30.glDeleteBuffers(bufferCount, pboIds, 0);
		GlUtil.checkGlError("glDeleteBuffers");

		LogUtil.LogD(TAG, "release   --->");
	}

	public static final int PBO_OFF = 0;
	public static final int PBO_1 = 1;
	public static final int PBO_2 = 2;

	private int mMode = PBO_OFF;

	public void setMode(int mode) {
		LogUtil.LogD(TAG, "setMode   <---");
		LogUtil.LogD(TAG, "setMode   mode = " + mode);
		mMode = mode;
		LogUtil.LogD(TAG, "setMode   --->");
	}

	private int pboIndex = 0;
	private final boolean VERBOSE = false;

	public void draw(GL10 gl) {
		if (mMode == PBO_OFF) {
			if (VERBOSE) {
				BenchmarkUtil.start("CopyData");
			}
			GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId);
			// start to copy pixels from system memory to texture object
			GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, IMAGE_WIDTH, IMAGE_HEIGHT, 0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, mBufferPixelData);
			if (VERBOSE) {
				BenchmarkUtil.stop("CopyData");
			}

			if (VERBOSE) {
				BenchmarkUtil.start("UpdateData");
			}
			// update data
			updatePixels(mBufferPixelData);
			if (VERBOSE) {
				BenchmarkUtil.stop("UpdateData");
			}
		} else {
			// "pboIndex" is used to copy pixels from a PBO to a texture object
			// "nextPboIndex" is used to update pixels in a PBO
			int nextPboIndex = 0;
			if (mMode == PBO_1) {
				// In single PBO mode, the index and nextIndex are set to 0
				pboIndex = nextPboIndex = 0;
			} else {
				// In dual PBO mode, increment current index first then get the next index
				pboIndex = (pboIndex + 1) % 2;
				nextPboIndex = (pboIndex + 1) % 2;
			}

			if (VERBOSE) {
				BenchmarkUtil.start("CopyData");
			}
			// bind the texture and PBO
			GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId);
			GLES30.glBindBuffer(GLES30.GL_PIXEL_UNPACK_BUFFER, pboIds[pboIndex]);
			// copy pixels from PBO to texture object
			GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, IMAGE_WIDTH, IMAGE_HEIGHT, 0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
			if (VERBOSE) {
				BenchmarkUtil.stop("CopyData");
			}

			if (VERBOSE) {
				BenchmarkUtil.start("UpdateData");
			}
			// bind PBO to update pixel values
			GLES30.glBindBuffer(GLES30.GL_PIXEL_UNPACK_BUFFER, pboIds[nextPboIndex]);
			ByteBuffer buffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_UNPACK_BUFFER, 0, DATA_SIZE, GLES30.GL_MAP_WRITE_BIT | GLES30.GL_MAP_INVALIDATE_BUFFER_BIT);
			// update data directly on the mapped buffer
			updatePixels(buffer);
			// release pointer to mapping buffer
			GLES30.glUnmapBuffer(GLES30.GL_PIXEL_UNPACK_BUFFER);
			if (VERBOSE) {
				BenchmarkUtil.stop("UpdateData");
			}

			// release PBOs with ID 0 after use.
			GLES30.glBindBuffer(GLES30.GL_PIXEL_UNPACK_BUFFER, 0);
		}

		// 使用shader程序
		GLES30.glUseProgram(handleProgram);
		GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
		GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId);

		// 指定顶点位置数据
		GLES30.glVertexAttribPointer(handlePosition, 3, GLES30.GL_FLOAT, false, 3 * GlUtil.SIZE_OF_FLOAT, mBufferVertex);
		// 指定纹理坐标数据
		GLES30.glVertexAttribPointer(handleTextureCoord, 2, GLES30.GL_FLOAT, false, 2 * GlUtil.SIZE_OF_FLOAT, mBufferTexture);
		// 指定变换矩阵
		GLES30.glUniformMatrix4fv(handleMVPMatrix, 1, false, MatrixUtil.getMVPMatrix(), 0);
		// 启用顶点位置数组
		GLES30.glEnableVertexAttribArray(handlePosition);
		// 启用纹理坐标数组
		GLES30.glEnableVertexAttribArray(handleTextureCoord);
		// 绘制三角形
		GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
		// 禁用顶点位置数组
		GLES30.glDisableVertexAttribArray(handleTextureCoord);
		// 禁用顶点颜色数组
		GLES30.glDisableVertexAttribArray(handlePosition);
	}

	private final int[] COLORS = new int[] { 
			Color.RED, 
			Color.GREEN, 
			Color.BLUE, 
			Color.YELLOW, 
			Color.CYAN, 
			Color.MAGENTA, };

	private int mColorIndex = 0;

	private int getColor() {
		int color = COLORS[mColorIndex];
		return color;
	}
	
	private int getNextColor() {
		int color = COLORS[mColorIndex];
		mColorIndex++;
		mColorIndex = mColorIndex % COLORS.length;
		return color;
	}

	private final boolean DEBGUG = false;
	private final int lineCount = 8;
	private final int rowsPerLine = IMAGE_HEIGHT / lineCount;
	private int mLineIndex = 0;
	private int pboCount = 0;

	private void updatePixels(ByteBuffer pixels) {
		if (null == pixels)
			return;
		if (DEBGUG) {
			updatePixelsByRow(pixels, 0, IMAGE_HEIGHT, getNextColor());
		} else {
			if (mMode == PBO_2) {
				pboCount = pboCount % 2;
				if (pboCount == 0) {
					updatePixelsByRow(pixels, mLineIndex * rowsPerLine, rowsPerLine, getNextColor());
				} else {
					updatePixelsByRow(pixels, mLineIndex * rowsPerLine, rowsPerLine, getColor());
					mLineIndex++;
				}
				pboCount++;
			} else {
				updatePixelsByRow(pixels, mLineIndex * rowsPerLine, rowsPerLine, getNextColor());
				mLineIndex++;
			}
			mLineIndex = mLineIndex % lineCount;
		}
	}

	private void updatePixelsByRow(ByteBuffer pixels, int fromRow, int rowCount, int color) {
		int bytesPerRow = IMAGE_WIDTH * GlUtil.BYTES_PER_PIXEL;
		int startIndex = fromRow * bytesPerRow;
		int endIndex = startIndex + rowCount * bytesPerRow;

		byte red = intToByte(Color.red(color));
		byte green = intToByte(Color.green(color));
		byte blue = intToByte(Color.blue(color));
		byte alpha = intToByte(Color.alpha(color));

		for (int i = startIndex; i < endIndex; i = i + GlUtil.BYTES_PER_PIXEL) {
			pixels.put(i, red);
			pixels.put(i + 1, green);
			pixels.put(i + 2, blue);
			pixels.put(i + 3, alpha);
		}
	}

	private byte intToByte(int value) {
		return (byte) value;
	}

}
